#!/usr/bin/env python
# -*- coding:utf-8 -*-


__author__ = 'Thom Wiggers, Luuk Scholten'
__email__ = 'thom@thomwiggers.nl, info@luukscholten.com'
__version__ = '1.1.0'

import asyncio
import hashlib
import logging
import mimetypes
import os
import re
from datetime import datetime
from http.client import responses
from urllib.parse import unquote
import functools

#-----------------------------------------
# HTTP常量定义
RESPONSE_CODE = 'code'
RESPONSE_HEADERS = 'headers'
RESPONSE_VERSION = 'version'
RESPONSE_CONTENT_LENGTH = 'Content-Length'
RESPONSE_BODY = 'body'
HTTP_VERSION = 'HTTP/1.1'


def tobytes(value):
    if isinstance(value, str):
        return value.encode('utf-8')
    assert(isinstance(value, bytes)), value
    return value

logger = logging.getLogger(__name__)

def _get_response(**kwargs):
    '''得到响应模板
    
    使用kwargs添加数据到字典
    '''
    if RESPONSE_CODE not in kwargs:
        kwargs[RESPONSE_CODE] = 200
    if RESPONSE_HEADERS not in kwargs:
        kwargs[RESPONSE_HEADERS] = {}
    if RESPONSE_VERSION not in kwargs:
        kwargs[RESPONSE_VERSION] = HTTP_VERSION
        
    return dict(**kwargs)

class HttpProtocol(asyncio.Protocol):
    '''HTTP/1.1 协议实现
    
    每一个连接
    '''
    
    def __init__(self, host, folder, event_loop = None, timeout = 15):
        '''初始化新实例
        
        Arguments:
            host: 服务器地址
            folder: 服务器文件目录
        '''
        self.host = host
        self.folder = folder
        self.logger = logger.getChild('HttpProtocol %s' % id(self))
        self.logger.debug("HttpProtocol初始化")
        self._loop = event_loop or asyncio.get_event_loop()
        self._timeout = timeout
        self._timeout_handle = None
        
    def _write_transport(self, bytes_value):
        self.transport.write(tobytes(bytes_value))
        
    def _write_response(self, response):
        '''向客户端写入返回信息
        
        Arguments:
            response: 包含回答信息的字典
        '''
        assert(isinstance(response, dict)), response
        status = '{} {} {}\r\n'.format(response[RESPONSE_VERSION],
                                       response[RESPONSE_CODE],
                                       'ddd',
                                       )
        self.logger.debug("应答状态: '%s'" % status.strip())
        self._write_transport(status)
        
        if RESPONSE_BODY in response and RESPONSE_CONTENT_LENGTH not in response[RESPONSE_HEADERS]:
            response[RESPONSE_HEADERS][RESPONSE_CONTENT_LENGTH] = len(response[RESPONSE_BODY])
            
        response['headers']['Date'] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
        for item in response[RESPONSE_HEADERS].items():
            self.logger.debug("  响应头: '%s: %s'" % item)
            self._write_transport('%s:%s\r\n' % item)
        
        self._write_transport('\r\n')
        if RESPONSE_BODY in response:
            self._write_transport(response[RESPONSE_BODY])
            
    def connection_made(self, transport):
        '''在连接创建时调用'''
        self.logger.info('连接由 %s 对象创建' % id(self))
        self.transport = transport
        self.keepalive = True
        
        if self._timeout:
            self.logger.debug("注册超时事件")
            self._timeout_handle = self._loop.call_later(self._timeout, self._handle_timeout)
            
    def connection_lost(self, exception):
        '''连接丢失或关闭时调用

        参数是异常或None.
        None 表示正常收到EOF, 或者表示连接在服务端被退出或关闭
        '''
        if exception:
            self.logger.exception('Connection lost!')
        else:
            self.logger.info('Connection lost')
            
    def data_received(self, data):
        '''处理从socket收到的数据
        
        在收到数据时被调用
        '''
        self.logger.debug("收到数据: %s" % repr(data))
        
        try:
            request = self._parse_headers(data)
            self._handle_request(request)
        except InvalidRequestError as e:
            self._write_response(e.get_http_response())
            
        if not self.keepalive:
            if self._timeout_handle:
                self._timeout_handle.cancel()
            self.transport.close()
        
        if self._timeout and self._timeout_handle:
            self.logger.debug("延迟超时事件")
            self._timeout_handle.cancel()
            self._timeout_handle = self._loop.call_later(self._timeout, self._handle_timeout)
            
    def _parse_headers(self, data):
        self.logger.debug('处理消息头')
        request_strings = list(map(lambda x: x.decode('utf-8'),
                                   data.split(b'\r\n')))
        request = {}
        
        # 解析request方法和HTTP版本
        method_line = request_strings[0].split()
        
        # 第一行应该有2-3个参数
        if not (2 <= len(method_line) <= 3):
            self.logger.info('无效http头')
            self.keepalive = False  # 不相信客户端
            raise InvalidRequestError(400, 'Bad request')
        # HTTP 0.9 不支持.
        if len(method_line) == 2:
            self.logger.info('是HTTP/0.9版本请求')
            self.keepalive = False  # HTTP/0.9不支持持久化
            raise InvalidRequestError(505, "This server only supports HTTP/1.0 and HTTP/1.1")
        else:
            request[RESPONSE_VERSION] = method_line[2]
        
        # method
        request['method'] = method_line[0]
        # request URI
        request['target'] = method_line[1]
        
        # 分析header
        for line in request_strings[1:]:
            if line == '':  #空行表示头结束
                break
            self.logger.debug("header: '%s'", line)
            header, body = line.split(': ', 1)
            request[header] = body
        
        self.logger.debug('request object: %s', request)
        return request
    
    def _get_request_uri(self, request):
        '''解析请求 URI into something useful
    
        Server MUST accept full URIs (5.1.2)'''
        request_uri = request['target']
        if request_uri.startswith('/'):  # eg. GET /index.html
            return (request.get('Host', 'localhost').split(':')[0],
                    request_uri[1:])
        elif '://' in request_uri:  # eg. GET http://rded.nl
            locator = request_uri.split('://', 1)[1]
            host, path = locator.split('/', 1)
            return (host.split(':')[0], path)
        

    def _handle_request(self, request):
        """Process the headers and get the file"""

        # Check if this is a persistent connection.
        connection = request.get('Connection')
        if connection:
            connection = connection.lower()
            self.keepalive = connection in ('keep-alive', 'close')
        #if request['version'] == 'HTTP/1.1':
            #self.keepalive = not request.get('Connection') == 'close'
        #elif request['version'] == 'HTTP/1.0':
            #self.keepalive = request.get('Connection') == 'Keep-Alive'

        # Check if we're getting a sane request
        if request['method'] not in ('GET', ):
            raise InvalidRequestError(501, 'Method not implemented')
        if request['version'] not in ('HTTP/1.0', 'HTTP/1.1'):
            raise InvalidRequestError(
                505, 'Version not supported. Supported versions are: {}, {}'
                .format('HTTP/1.0', 'HTTP/1.1'))

        host, location = self._get_request_uri(request)

        # We must ignore the Host header if a host is specified in GET
        if host is None:
            host = request.get('Host')

        # Check if this request is intended for this webserver
        if host is not None and not host == self.host:
            self.logger.info('Got a request for unknown host %s', host)
            raise InvalidRequestError(404, "We don't serve this host")

        filename = os.path.join(self.folder, unquote(location))
        self.logger.debug('trying to serve %s', filename)

        if os.path.isdir(filename):
            filename = os.path.join(filename, 'index.html')

        if not os.path.isfile(filename):
            raise InvalidRequestError(404, 'Not Found')

        # Start response with version
        response = _get_response(version=request['version'])

        # timeout negotiation
        match = re.match(r'timeout=(\d+)', request.get('Keep-Alive', ''))
        if match is not None:
            requested_timeout = int(match.group(1))
            if requested_timeout < self._timeout:
                self._timeout = requested_timeout

        # tell the client our timeout
        if self.keepalive:
            response['headers']['Keep-Alive'] = 'timeout={}'.format(self._timeout)

        # Set Content-Type
        response['headers']['Content-Type'] = mimetypes.guess_type(filename)[0] or 'text/plain'

        # Generate E-tag
        sha1 = hashlib.sha1()
        with open(filename, 'rb') as fp:
            response['body'] = fp.read()
            sha1.update(response['body'])
        etag = sha1.hexdigest()

        # Create 304 response if if-none-match matches etag
        if request.get('If-None-Match') == '"{}"'.format(etag):
            # 304 responses shouldn't contain many headers we might already
            # have added.
            response = _get_response(code=304)

        response['headers']['Etag'] = '"{}"'.format(etag)

        self._write_response(response)

    def _handle_timeout(self):
        """Handle a timeout"""
        self.logger.info('Request timed out')
        self.transport.close()


class InvalidRequestError(Exception):
    """Raised for invalid requests. Contains the error code.

    This exception can be transformed to a http response.
    """

    def __init__(self, code, *args, **kwargs):
        """Configures a new InvalidRequestError

        Arguments:
            code -- the HTTP error code
        """
        super(InvalidRequestError, self).__init__(*args, **kwargs)
        self.code = code

    def get_http_response(self):
        """Get this exception as an HTTP response suitable for output"""
        return _get_response(
            code=self.code,
            body=str(self),
            headers={
                'Content-Type': 'text/plain'
            }
        )

    
def _start_server(bindaddr, port, hostname, folder):
    """Starts an asyncio server"""
    import asyncio
    loop = asyncio.get_event_loop()
    assert(isinstance(loop, asyncio.base_events.BaseEventLoop))
    coroutine = loop.create_server(functools.partial(HttpProtocol, hostname, folder),
                                   bindaddr,
                                   port)
    server = loop.run_until_complete(coroutine)

    print('Starting server on {}'.format(server.sockets[0].getsockname()))
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass


def run(argv=None):  # pragma: no cover
    """Run the HTTP server
    
    Usage:
        httpserver [options] [<folder>]
    
    Options::
    
        -h,--host=<hostname>        What host name to serve (default localhost)
        -a,--bindaddress=<address>  Address to bind to (default 127.0.0.1)
        -p,--port=<port>            Port to listen on (default 8080)
        -v,--verbose                Increase verbosity to INFO messages
        -d,--debug                  Increase verbosity to DEBUG messages
        --help                      Print this help message
        --version                   Print the version
    
    To serve /path/to/www on all (ipv4) addresses for host myserver
    on port 80::
        httpserver -a 0.0.0.0 -p 80 -h myserver /path/to/www
    """
    import sys
    import os
    import argparse
    import textwrap
    
    # Check for the version
    if not sys.version_info >= (3, 4):
        print('This python version is not supported. Please use python 3.4')
        exit(1)
    
    argv = argv or sys.argv[1:]
    
    # remove some RST formatting
    parser = argparse.ArgumentParser()
    parser.add_argument('folder', nargs = '*', default=r"F:\Help\CGAL-4.9-doc_html\Manual")
    parser.add_argument('--host', default='localhost', help='What host name to serve')
    parser.add_argument('-a', '--bindaddress', default='127.0.0.1', help='Address to bind to')
    parser.add_argument('-p', '--port', default='8080', help='Port to listen on')
    parser.add_argument('-v', '--verbose', help='Increase verbosity to INFO messages')
    parser.add_argument('-d', '--debug', default = True, help='Increase verbosity to DEBUG messages')
    parser.add_argument('--version', help='Print the version')
    args = parser.parse_args()
    #args = docopt.docopt(textwrap.dedent(docblock), argv)
    
    if args.version:
        print("httpserver version {} by {}".format(
            __version__,
            __author__))
        exit(0)
    if 'help' in args:
        print("httpserver version {} by {}".format(
            __version__,
            __author__))
        exit(0)
    
    # Set up logging
    level = logging.WARNING
    if args.verbose:
        level = logging.INFO
    if args.debug:
        level = logging.DEBUG
    logging.basicConfig(level=level)
    
    logger = logging.getLogger('run method')
    logger.debug('CLI args: %s' % args)
    
    bindaddr = args.bindaddress
    port = args.port
    if args.folder:
        if isinstance(args.folder, str):
            folder = args.folder
        else:
            folder = args.folder[0]
    else:
        folder = os.getcwd()
    #folder = args.folder[0] or os.getcwd()
    hostname = args.host
    _start_server(bindaddr, port, hostname, folder)
    
if __name__ == '__main__':
    run()